import { fromObject } from 'convert-source-map'
import { isObject, isString } from 'lodash-es'
import path, { posix as posixPath } from 'path'
import { commonInputFS } from '../common'
import getRollupPlugin from '../rollup'
import { Awaitable, CompluginContextMeta, CompluginOptions, CreateCompluginArgs, EmitAssetArgs } from '../types'
import {
  addSourceComment,
  CWD,
  doHash,
  doThen,
  getExtname,
  isPromiseLike,
  loadDependency,
  lookupName,
  makeRelativeLegalPosixPath,
  toDataUri
} from '../utils'
import { toBuffer } from './../utils'

const FS_PREFIX = `/@fs/`
const ASSET_PREFIX = `/@asset/`
const priFsKey = Symbol.for('complugin:vite[0]')
const priAssetMapKey = Symbol.for('complugin:vite[1]')
const priDataKey = Symbol.for('complugin:vite[data]')

function fileToDevUrl(id: string, config: import('vite').ResolvedConfig) {
  let rtn: string
  if (id.startsWith(config.root)) {
    // in project root, infer short public path
    rtn = '/' + posixPath.relative(config.root, id)
  } else {
    // outside of project root, use absolute fs path
    // (this is special handled by the serve static middleware
    rtn = posixPath.join(FS_PREFIX + id)
  }
  const origin = config.server?.origin ?? ''
  return origin + config.base + rtn.replace(/^\//, '')
}

export default function getVitePlugin<UserOptions = {}>(args: CreateCompluginArgs<UserOptions>, options?: UserOptions) {
  let watchMode = false
  let env: import('vite').ConfigEnv
  let config: import('vite').ResolvedConfig
  let server: import('vite').ViteDevServer
  let previewServer: (CompluginContextMeta & { framework: 'vite' })['previewServer']
  let raw: CompluginOptions
  let base: string

  const oldFactory = args.factory

  const plugin = getRollupPlugin(
    {
      ...args,
      factory(...args) {
        raw = oldFactory.call(this, ...args)
        return raw
      }
    },
    options
  )

  const vitePlugin: Partial<Omit<import('vite').Plugin, keyof import('rollup').Plugin>> = {
    config(_config, _env) {
      env = _env
    },
    configResolved(_config) {
      config = _config
      base = config.base

      plugin.$complugin.sourceMap = Boolean(config.build.sourcemap || config.build.rollupOptions.output)
    },
    configureServer(_server) {
      server = _server
      watchMode = true

      let assetMap = new Map<string, Awaitable<string | Uint8Array>>()

      if ((server as any)[priAssetMapKey]) {
        assetMap = (server as any)[priAssetMapKey]
      } else {
        assetMap = (server as any)[priAssetMapKey] = new Map()
        server.middlewares.use(ASSET_PREFIX, (req, res, next) => {
          let url = req.url

          if (!url) return next()
          if (url.startsWith('/')) {
            url = url.slice(1)
          }
          const asset = assetMap.get(url)

          if (!asset) return next()
          return doThen(asset, source => {
            const mimeType = lookupName(url)
            res.writeHead(200, {
              'Content-Type': /^text\//.test(mimeType) ? `${mimeType}; charset=utf-8` : mimeType,
              'Access-Control-Allow-Origin': '*'
            })

            return void res.end(source)
          })
        })
      }

      const { moduleGraph } = server

      const emitAsset = (asset: EmitAssetArgs, id?: string) => {
        let { fileName, name, source = '' } = asset

        if (!fileName) {
          name ||= 'unknown'
          const extname = getExtname(name)
          if (isPromiseLike(source)) {
            fileName = `name-${doHash([name, id || '', CWD])}${extname}`
          } else {
            fileName = `name-${doHash((source as any).code ?? (source as any))}${extname}`
          }
        } else {
          fileName = makeRelativeLegalPosixPath(fileName)
        }

        assetMap.set(
          fileName,
          Promise.resolve(source).then(source => {
            if (isString(source) || source instanceof Uint8Array) {
              return source
            } else if (isObject(source)) {
              let { code, map } = source
              if (!config.build.sourcemap) return code

              map = isString(map) ? JSON.parse(map) : map
              if (map) {
                code = addSourceComment(
                  typeof code === 'string' ? code : toBuffer(code).toString(),
                  fromObject(map).toComment({ multiline: true })
                )
              }

              return code
            } else {
              return ''
            }
          })
        )

        return JSON.stringify(ASSET_PREFIX + fileName)
      }

      const rollupGetContext = plugin.$complugin.getContext
      const rollupGetMinifyContext = plugin.$complugin.getMinifyContext

      plugin.$complugin.getMinifyContext = function (hook, context) {
        return {
          ...rollupGetMinifyContext.call(this, hook, context),
          emitFile: asset => emitAsset(asset)
        }
      }
      plugin.$complugin.getContext = function (hook, id, context) {
        return {
          ...rollupGetContext.call(this, hook, id, context),
          emitStatic(id) {
            const url = fileToDevUrl(id, config)
            return JSON.stringify(url)
          },
          emitAsset: asset => emitAsset(asset, id)
        }
      }
      plugin.$complugin.resolveAssetId = (asset, context) => {
        const code = asset.source?.toString() ?? ''
        try {
          context.parse(code)
          return { id: toDataUri(code, 'text/javascript') }
        } catch {}

        const warn = `[complugin:vite '${args.name}'|resolveId]: serve mode is not support asset module.`
        context.warn(warn)
        return { id: toDataUri(`console.warn(${JSON.stringify(warn)});export {}`, 'text/javascript') }
      }
      plugin.$complugin.rebuild = id => {
        const module = moduleGraph.getModuleById(id)
        if (module) {
          moduleGraph.invalidateModule(module)
        }
      }
    },
    configurePreviewServer(_server) {
      previewServer = _server
    }
  }

  plugin.$complugin.framework = 'vite'

  const rollupGetMeta = plugin.$complugin.getMeta
  plugin.$complugin.getMeta = function () {
    let data: any
    let inputFS: any

    return Object.create(null, {
      ...Object.getOwnPropertyDescriptors(rollupGetMeta.call(this)),
      ...Object.getOwnPropertyDescriptors({
        plugin,
        config,
        server,
        previewServer,
        env,
        watchMode,
        root: path.resolve(config.root),
        isProd: env.mode === 'production',
        get version() {
          return loadDependency('vite/package.json')?.version
        },
        get base() {
          return base
        },
        get data() {
          return data ?? (config as any)[priDataKey] ?? ((config as any)[priDataKey] = Object.create(null))
        },
        get inputFS() {
          return inputFS ?? (inputFS = (config as any)[priFsKey] ?? ((config as any)[priFsKey] = { ...commonInputFS }))
        }
      })
    })
  }

  Object.assign(plugin, vitePlugin)
  return plugin as import('vite').Plugin
}
